Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make hooks better 🦄 #523

Closed
wants to merge 3 commits into from

Conversation

szmarczak
Copy link
Collaborator

@szmarczak szmarczak commented Jul 15, 2018

  • got.hooks = got.defaults.options.hooks (all defaults are deep frozen except hooks)
  • By default hooks are always empty
    (before: if you hadn't specified hooks in the defaults, got.hooks would be undefined)
  • Improved hook type checking
  • New hooks: onSocketConnect, onAbort

@sindresorhus
Copy link
Owner

// @jstewmon

);
}
for (const [hookEvent, hooks] of Object.entries(options.hooks)) {
if (is.array(hooks)) {
Copy link
Contributor

@jstewmon jstewmon Jul 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails to validate the known hooks if something other than an array is provided, which will result in an error upstream.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks - I got tripped up reviewing the diff.

}
for (const [hookEvent, hooks] of Object.entries(options.hooks)) {
if (is.array(hooks)) {
for (const [index, hook] of Object.entries(hooks)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not equivalent to the current implementation. Consider these examples:

const a = [1, 2];
a.foo = 'bar';
Object.entries(a); // [ [ '0', 1 ], [ '1', 2 ], [ 'foo', 1 ] ]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a bug. It's a feature!

@@ -132,7 +132,7 @@ test('throws TypeError when known `hooks` array item is not a function', async t
});

test('allows extra keys in `hooks`', async t => {
await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: {}}}));
await t.notThrows(() => got(`${s.url}/test`, {hooks: {extra: []}}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this test is to show that extra keys are not required to pass validation b/c they should be ignored. The test title could be more clear.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would someone assign non-hook object to hooks? What's the use case? What big problem does this solve? Sorry, but I don't see that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It allows for encapsulation. Here's a contrived example:

class GotHooks {

  constructor () {
    this.message = 'Encapsulated Got Hooks';
    this.beforeRequest = [
      () => this.runRook()
    ];
  }

  async runRook() {
    console.log(this.message);
  }
}

If got validates only the properties it is concerned with, then everything is fine. If got extraneously demands that the object only have properties that correspond to known hooks, then the user has to jump through some hoops to create an object that accomplishes the same thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the detailed response, I really appreciate that! 👍

@@ -150,7 +157,9 @@ module.exports = (options = {}) => {

const socket = req.connection;
if (socket) {
const onSocketConnect = () => {
const onSocketConnect = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the origin of this conditional block are, but I think the right way to handle this event is to remove the if block and make this:

req.on('socket', () => { ... });

https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_socket

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, since onSocketConnect and onAbort should be attached to the request, I don't think these are sensible hooks for got to configure directly. If you want to configure these for a got instance, you really just need to configure a listener for request and attach the request event listeners in the request listener.

Copy link
Collaborator Author

@szmarczak szmarczak Jul 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the origin of this conditional block are, but I think the right way to handle this event is to remove the if block

#429

Actually, since onSocketConnect and onAbort should be attached to the request, I don't think these are sensible hooks for got to configure directly. If you want to configure these for a got instance, you really just need to configure a listener for request and attach the request event listeners in the request listener.

Why would you do the same thing twice? Oh wait. I do that twice. I'll change that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reference to #429. Maybe I'm missing something, but I don't think that was the right fix b/c if req.connection is just testing a race condition. Shouldn't it be:

req.once('socket', (sock) => {
  sock.once('connect', conn => {
    // ...
  }
};

I can see the convenience of low-friction hooks. Maybe there's a way to make the configuration a sort of DSL that got can use to wire these up. Something like this:

{
  events: {
    request: {
      ['on|once']: {
        socket: {
          null: [(sock) => { console.log('request.on(socket)') }],
          connect: [(conn) => { console.log('socket.on(connect)') }]
        }
      },
    }
  }
}

With the above, the configuration can be traversed and the listeners can be subscribed without having to add handling code to got for every event.

I used null as a sentinel value to mean the subscriber for the event as opposed to a child subscription. Something else (symbol?) would work also.

Copy link
Collaborator Author

@szmarczak szmarczak Jul 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but I don't think that was the right fix b/c if req.connection is just testing a race condition. Shouldn't it be:

I don't know. Can you make another issue for that?

Copy link
Collaborator Author

@szmarczak szmarczak Jul 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the convenience of low-friction hooks.

Great! We'll discuss this later. I was gonna send a PR with rewrited hooks from scratch but I decided current implementation is better. :)

@jstewmon
Copy link
Contributor

I really like being able to configure event listeners on a per-instance basis - it's really useful for being able to add instrumentation to a client without having to litter every request site with the event listeners.

I have a few questions regarding the implementation:

  • Should these go under hooks? These are actually EE listeners, so it might be better to give them a discrete top-level config key like listeners. This would also disambiguate them from hooks, which are stated to allow modification during the pipeline.
  • The EE listeners can't modify anything, so they should not be awaited even if they are async.
  • I don't think it makes sense to have some aspects of instance configuration be frozen while others are not. Pick one - frozen or unfrozen. I don't see a practical reason why the configuration is frozen... Freezing makes perfect sense when there is state synchronization that happens during instance setup, which would cause modifications to the configuration to be ignored by the instance, but I don't think that's the case with got, so I'd say got should not freeze configuration. If a user wants to ensure the config is not modified, they can freeze it themselves when they create the instance.

@szmarczak
Copy link
Collaborator Author

szmarczak commented Jul 16, 2018

Should these go under hooks? These are actually EE listeners, so it might be better to give them a discrete top-level config key like listeners. This would also disambiguate them from hooks, which are stated to allow modification during the pipeline.

You're right.

The EE listeners can't modify anything, so they should not be awaited even if they are async.

I don't understand. EE listeners aren't awaited.

I don't think it makes sense to have some aspects of instance configuration be frozen while others are not. Pick one - frozen or unfrozen. I don't see a practical reason why the configuration is frozen...

It's frozen to prevent unwanted changes to the instance. But yeah, some people might want to keep them frozen and some people might want to not. My proposal: to enable freezing people will need to set defaults.preventChanges to true (false by default).

@jstewmon
Copy link
Contributor

I don't understand. EE listeners aren't awaited.

You're using callAll, which awaits the hook function, in response to an EE event.

@jstewmon
Copy link
Contributor

Can we agree that the options should either be frozen or not? If so, then I think you can cut down this PR by reverting the changes to how the configuration is setup and normalized, right? That will allow us to focus on the interface for the event listeners.

@szmarczak
Copy link
Collaborator Author

I'm closing this due to a GitHub crash. This PR doesn't see new commits. I'll create another PR when I'm done with fixing things. Big thanks to @jstewmon for letting me know what I've done wrong! 🙌

@szmarczak szmarczak closed this Jul 16, 2018
@szmarczak
Copy link
Collaborator Author

You're using callAll, which awaits the hook function, in response to an EE event.

It's fixed in the upcoming PR.

@szmarczak
Copy link
Collaborator Author

Can we agree that the options should either be frozen or not?

My answer: defaults.preventChanges. The default value still needs to be discussed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants